Laboratorio 6: La desperación de Mr. Lepin 🐼

MDS7202: Laboratorio de Programación Científica para Ciencia de Datos

Cuerpo Docente:¶

  • Profesor: Matías Rojas y Mauricio Araneda
  • Auxiliar: Ignacio Meza D.
  • Ayudante: Rodrigo Guerra

Equipo: SUPER IMPORTANTE - notebooks sin nombre no serán revisados¶

  • Nombre de alumno 1: Johnny Godoy

Link de repositorio de GitHub: https://github.com/johnny-godoy/laboratorios-mds/blob/main/lab%206/laboratorio_6.ipynb¶

Indice¶

  1. Temas a tratar
  2. Descripcción del laboratorio
  3. Desarrollo

Temas a tratar¶

  • Aplicar Pandas para obtener características de un DataFrame.
  • Aplicar Pipelines.
  • Aplicar Clusters sobre un conjunto de datos.

Reglas:¶

  • Fecha de entrega: 09/06/2021
  • Grupos de 2 personas
  • Ausentes deberán realizar la actividad solos.
  • Cualquier duda fuera del horario de clases al foro. Mensajes al equipo docente serán respondidos por este medio.
  • Prohibidas las copias.
  • Pueden usar cualquer matrial del curso que estimen conveniente.
  • Código que no se pueda ejecutar, no será revisado.

Objetivos principales del laboratorio¶

  • Comprender y aprovechar las ventajas que nos ofrece la librería pandas con respecto a trabajar en Python 'puro'.
  • Crear nuevas características para entrenar un modelo de clustering.
  • Comprender como aplicar pipelines de Scikit-Learn para generar procesos más limpios.

El laboratorio deberá ser desarrollado sin el uso indiscriminado de iteradores nativos de python (aka "for", "while"). La idea es que aprendan a exprimir al máximo las funciones optimizadas que nos entrega numpy, las cuales vale mencionar, son bastante más eficientes que los iteradores nativos sobre arreglos (o tensores).

Descripción del laboratorio.¶

Importamos librerias utiles 😸¶

In [1]:
import datetime

import numpy as np
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.cluster import KMeans
from sklearn.compose import ColumnTransformer
from sklearn.exceptions import NotFittedError
from sklearn.impute import SimpleImputer
from sklearn.manifold import TSNE
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import FunctionTransformer

from IPython.display import HTML

Segmentación de Clientes en Tienda de Retail 🛍️¶

1.1 Cargar Dataset¶

Mr. Lepin, en una nueva reunión, le cuenta a ud y su equipo que los resultados derivados del análisis exploratorio de dato presentaron una gran utilidad para la empresa y que tiene un gran entusiasmo por continuar trabajando con ustedes. Es por esto, que Mr. Lepin les pide que cargue y visualicen algunas de las filas que componen el Dataset. A continuación un extracto de lo parlamentado en la reunión:

- Usted: Es un gran logro para nuestro equipo que usted haya encontrado excelente el EDA. ¿Qué tiene en mente ahora?
- Mr. Lepin: Resulta que hace algún tiempo, mientras tomaba un mojito en una reunión de gerentes en Panamá, oí a un *chato* acerca de **LRMFP**, que es un modelo que permite personificar a los clientes a través de la farbicación de distintos atributos que describen a los clientes. Lo encontré es-tu-pendo ñatito. 
- Usted: Ehh bueno. Investigaremos acerca de este modelo y veremos lo que podemos hacer.

Por ende, su siguiente tarea es calcular LRMFP sobre cada cliente y luego hacer un análisis de las características generadas. Para esto, el área de ventas les entrega un nuevo archivo llamado online_retail_II_cleaned.pickle, quien posee los datos del DataFrame original limpios y listos para obtener las características solicitadas por Mr. Lepin.

In [2]:
df_retail = pd.read_pickle("data/online_retail_II_cleaned.pickle")
df_retail = df_retail.astype(
    {
        "Invoice": "category",
        "StockCode": "category",
        "Description": "category",
        "Description": str,
        "Customer ID": "category",
        "Country": "category"
    }
)
df_retail.head()
Out[2]:
Invoice StockCode Description Quantity InvoiceDate Price Customer ID Country
0 489434 85048 15CM CHRISTMAS GLASS BALL 20 LIGHTS 12 2009-12-01 07:45:00 6.95 13085.0 United Kingdom
1 489434 79323P PINK CHERRY LIGHTS 12 2009-12-01 07:45:00 6.75 13085.0 United Kingdom
2 489434 79323W WHITE CHERRY LIGHTS 12 2009-12-01 07:45:00 6.75 13085.0 United Kingdom
3 489434 22041 RECORD FRAME 7" SINGLE SIZE 48 2009-12-01 07:45:00 2.10 13085.0 United Kingdom
4 489434 21232 STRAWBERRY CERAMIC TRINKET BOX 24 2009-12-01 07:45:00 1.25 13085.0 United Kingdom

1.2 Creación de nuevas Caracteristicas [2 Puntos]¶

Como ya se les comento, Mr. Lepin esta interesado en obtener las características LRMFP, para esto les señala que estas características se construyen en base a las siguientes definiciones:

  • Length (L): Intervalo de tiempo, en días, entre la primera y la última visita del cliente. Mientras mas grande sea el valor, mas fiel es el cliente.
  • Recency (R): Indica la actualidad de la interacción de un cliente con la empresa, y da información sobre la tendencia a repetir la compra. Se define como: $$Recency(n)=\dfrac{1}{n} \sum^n_{i=1} date\_diff(t_{fecha final}, t_{m-i+1})$$

    Donde $date\_diff$ representa la diferencia en días entre la fecha de finalización del periodo de observación ($t_{fecha final}$), y la fecha de una visita del cliente cercana a $t_{fecha final}$, $t_{m-i+1}; t_{m}$ es la última visita del cliente; y n es el número de visitas recientes del cliente consideradas.

  • Monetary (M): El término "monetario" se refiere a la cantidad media de dinero gastada por cada visita del cliente durante el período de observación y refleja la contribución del cliente a los ingresos de la empresa.

  • Frequency (F): Se refiere al número total de visitas del cliente durante el periodo de observación. Cuanto mayor sea la frecuencia, mayor será la fidelidad del cliente.

  • Periodicity (P): Representa si los clientes visitan las tiendas con regularidad.

$$Periodicity(n)=std(IVT_1, ..., IVT_n)$$

         Donde $IVT$ denota el tiempo entre visitas y n representa el número de valores de tiempo entre visitas de un cliente.

$$IVT_i=date\_diff(t_{i+1},t)$$

En base a las definiciones señaladas, diseñe una función que permita obtener las características LRMFP recibiendo un DataFrame como entrada. Para esto, no estará permitido el uso de iteradores, utilice todas las herramientas que les ofrece pandas para realizar esto.

Una referencia que le puede ser útil es el documento original en donde se propone este método.

Nota: Para la $fechafinal$ utilice la fecha máxima del dataset más 1 día.

Ejemplo de Resultado Esperado:

Customer ID Length Recency Frequency Monetary Periodicity
12346.0 294 67 46 -64.68 37.0
12347.0 37 3 71 1323.32 0.0
12349.0 327 43 107 2646.99 78.0
12352.0 16 11 18 343.80 0.0
12356.0 44 16 84 3562.25 12.0

Respuesta:

In [3]:
def custom_features(dataframe_in: pd.DataFrame, m: int = None) -> pd.DataFrame:
    """Retorna un frame con las características del modelo LRMFP.

    Parametros
    ----------
    dataframe_in: pd.DataFrame
        frame con los datos de retail.
    m : int, optional
        Número de visitas consideradas para calcular recencia.
        Si es None, entonces se calcula con el máximo entre 1 y
        la frecuencia mínima.

    Retorna
    -------
    df_out: pd.DataFrame
        frame con las características LRMFP."""
    dataframe_copy = dataframe_in[["Customer ID", "InvoiceDate"]].copy()
    dataframe_copy["prod"] = dataframe_in.Price*dataframe_in.Quantity
    df_out = pd.DataFrame(index=dataframe_in["Customer ID"].unique().sort_values())
    by_customer = dataframe_copy.sort_values(by="InvoiceDate").groupby("Customer ID")

    dates = by_customer.InvoiceDate
    df_out["Length"] = (dates.max() - dates.min()).dt.days

    freq = dates.count()
    if m is None:
        m = max(1, freq.min())
    last_date = df_retail.InvoiceDate.max() + datetime.timedelta(days=1)
    df_out["Recency"] = (last_date - by_customer.tail(m).groupby("Customer ID").InvoiceDate.mean()).dt.days

    df_out["Frecuency"] = freq
    df_out["Monetary"] = by_customer["prod"].mean()

    dataframe_copy["diff_time"] = dates.diff().dt.days
    df_out["Periodicity"] = dataframe_copy.groupby("Customer ID").diff_time.std()

    return df_out

1.3 Pipelines 👷¶

Finalmente Don Mora le pregunta si seria posible realizar un pipeline para realizar una segmentación de los clientes con los nuevos datos generados, a lo que usted responde que sí y propone la utilización de k-means para la segmentación.

A continuación siga los pasos requeridos para obtener la segmentación de clientes.

1.3.1 Estandarizar Caracteristicas [0.5 puntos]¶

Construya una clase llamada MinMax() utilizando BaseEstimator y TransformerMixin para realizar una transformación de cada una de las columnas de un DataFrame utilizando ColumnTransformer() más tarde (tome como referencia el siguiente enlace).

Para esto considere que Min-Max escaler queda dada por la ecuación:

$$MinMax = \dfrac{x-min(x)}{max(x) - min(x)}$$

Con esto buscamos que los valores que componen a las columnas se muevan en el rango de valores $[0, 1]$.

Respuesta:

In [4]:
class MinMax(BaseEstimator, TransformerMixin):
    """Escala y transforma cada característica tal que quede en el rango [0, 1]
    en el conjunto de entrenamiento."""
    def fit(self, X, y=None):
        """Calcula el mínimo y máximo para ser usado en escalamiento.

        Parametros
        ----------
        X: array-like de forma (n_muestras, n_características)
            Los datos usados para calcular el mínimo y máximo por característica
            que se usan para escalar.
        y: None
            Ignorado, por compatibilidad.

        Retorna
        -------
        self: MinMax
            El escalador entrenado."""
        self.min_ = np.nanmin(X, axis=0)
        self.denominator_ = np.nanmax(X, axis=0) - self.min_
        return self

    def transform(self, X):
        """Escala las características de X según el mínimo y máximo encontrado.

        Parametros
        ----------
        X: array-like de forma (n_muestras, n_características)
            Datos de entrada a ser transformados.

        Retorna
        -------
        Xt: array-like de forma (n_muestras, n_características)
            Datos transformados.

        Levanta
        -------
        NotFittedError: Si es que el modelo no ha llamado al método fit antes."""
        try:
            Xt = (X - self.min_)/self.denominator_
            return Xt
        except AttributeError:
            raise NotFittedError("Esta instancia de MinMax no está entrenada todavía. "
                                 "Llame a `fit` con los argumentos apropiados antes de usar este transformador")

1.3.2 T-SNE Pipeline [1.0 puntos]¶

Para comenzar introduciéndose en el uso de pipeline, decide probar realizando un pipeline enfocado en la reducción de dimensionalidad y así hacer no decepcionar a Mr. Lepin con la clusterización del modelo.

Configure un pipeline utilizando el algoritmo T-SNE sobre los datos LRMFP, donde, para la realización del pipeline considera los siguientes pasos:

  1. Como primer paso obtenga las características LRMFP desde el DataFrame df_retail_II_cleaned.pickle utilizando la función custom_features creada anteriormente, junto a FunctionTransformer(). Considere esto como el primer paso de su pipeline.
  2. En segundo lugar usando ColumnTransformer() aplique el MinxMax scaler creado por usted sobre todas las columnas generadas en el paso anterior.
  3. Finalmente, aplique un último paso donde obtiene las 2 componentes más relevantes utilizando el algoritmo T-sne de sckit-learn.

Tras aplicar las transformaciones sobre el dataset LRMFP, gráfique las componentes obtenidas en la reducción de dimensionalidad.

Aplicando la transformaciones

In [5]:
preprocessing = Pipeline(steps=[("feature_transformer", FunctionTransformer(custom_features)),
                                ("imputer", SimpleImputer(strategy="mean")),
                                ("scaler", MinMax()),
                               ],
                        )
dimensionality_reduction = Pipeline(steps=[("preprocessing", preprocessing),
                                           ("reducer", TSNE(n_components=2, random_state=0,
                                                            learning_rate="auto", init="random",
                                                           )
                                           ),
                                          ],
                                   )
X_reduced = pd.DataFrame(dimensionality_reduction.fit_transform(df_retail), columns=["x", "y"])
X_reduced
Out[5]:
x y
0 8.378868 -42.816170
1 -1.798583 21.489256
2 -71.841568 8.571382
3 21.666998 -18.503113
4 -9.306869 39.164833
... ... ...
4309 53.665306 -13.795135
4310 -68.078026 20.554010
4311 -33.021099 52.639996
4312 24.813828 -38.750008
4313 21.537745 -6.155716

4314 rows × 2 columns

Visualizando el espacio de dimensión reducida

In [6]:
fig = px.scatter(X_reduced, x="x", y="y")
fig.update_layout(title="Espacio de dimensión reducida con T-SNE")

1.3.3 Clustering¶

1.3.3.1 Método del Codo [1 puntos]¶

Utilizando la clase creada para escalamiento, aplique el método del codo para visualizar cual es el número de clusters que mejor se ajustan a los datos. Realice esto utilizando el algoritmo K-means dentro de un pipeline para un $k \in [1,20]$, donde k representa el número de clusters del k-means. Para la realización de esta sección y la próxima (1.3.3.2), considere los mismos pasos utilizados para el t-sne, pero permutando el algoritmo de reducción de dimensionalidad por k-means.

A través del grafico obtenido, comente y justifique que valor de k escogería para realizar el k-means.

Creando el pipeline de KMeans

In [7]:
def process_k_means(n_clusters: int) -> Pipeline:
    clst = Pipeline(steps=[("preprocessing", preprocessing),
                           ("cluster", KMeans(n_clusters=n_clusters, random_state=0)),
                          ],
                   ).fit(df_retail)
    return clst

Calculando las inercias

In [8]:
kvals = np.arange(1, 20)
clustering_models = [process_k_means(k) for k in kvals]
inertias = [clst.named_steps["cluster"].inertia_ for clst in clustering_models]

Visualizando el cambio de inercia.

In [9]:
fig = px.line(x=kvals, y=inertias)
fig.update_layout(title="Método del codo para determinar cantidad de clusters",
                  xaxis_title="Cantidad de clusters", yaxis_title="Inercia")
fig.show()

Se elige como valor óptimo $k=3$, pues a partir de este punto los beneficios ganados por aumentar los cluster disminuyen considerablemente.

In [10]:
optimal_k = 3

1.3.3.2 Segmentación de Clientes con K-Means 🎁 [1 punto]¶

En base a la elección de k realizada en la sección anterior, utilice este valor escogido y entrene un modelo de K-means utilizando el mismo pipeline de scikit-learn utilizado anteriormente.

Una vez ajustado los datos, genere una tabla con los promedios (o medianas) para cada uno de los atributos, agrupando estos por el clúster que pertenecen. ¿Es posible observar agrupaciones coherentes?, ¿Qué tipo de clientes posee el retail?, Justifique su respuesta y no decepcione a Mr. Lepin.

Respuesta:

Generando agrupaciones por cluster

In [11]:
optimal_model = clustering_models[optimal_k - 1]
df_clusters = custom_features(df_retail)
labels = optimal_model.named_steps["cluster"].labels_
df_clusters["Cluster"] = labels
by_cluster = df_clusters.groupby("Cluster")

Viendo la cantidad de puntos en cada cluster

In [12]:
by_cluster.size()
Out[12]:
Cluster
0     959
1    1779
2    1576
dtype: int64

La media

In [13]:
by_cluster.mean()
Out[13]:
Length Recency Frecuency Monetary Periodicity
Cluster
0 23.645464 251.627737 28.454640 53.258432 3.940067
1 277.280495 37.318156 167.035413 32.357707 21.125143
2 39.241117 54.590736 48.542513 32.311214 5.395593

La mediana

In [14]:
by_cluster.median()
Out[14]:
Length Recency Frecuency Monetary Periodicity
Cluster
0 0.0 245.0 19.0 16.870000 0.000000
1 278.0 25.0 97.0 18.077614 15.854776
2 0.0 46.0 29.0 16.788990 0.397565
  • El cluster 0 se caracteiza por tener un Recency mucho más alto que el resto. La media de su Monetary igualmente es alta, pero la mediana no, por lo que puede ser por clientes outlier. Estos son los que tienen mayor tendencia a repetir la compra.
  • El cluster 1 se caracteriza por tener alto Length, Frecuency y Periodicity: Son clientes de alta fidelidad que visitan con regularidad.
  • EL cluster 2 tiene un Recency intermedio a los otros dos clusters, y lo mismo con Frecuency. Son clientes que tienen mayor tendencia a repetir la compra que los de alta fidelidad, pero tienen menor frecuencia.

Respuesta Esperada:

Length Recency Frequency Monetary Periodicity
Cluster
0 258.8 45.2 76.1 1107.7 107.6 449
1 76.1 217.6 45.5 791.7 14.1 466
2 368.5 4.8 2715.0 226621.6 4.2 4
3 85.3 45.7 65.8 1047.0 10.5 987
4 347.2 15.9 1658.0 35829.3 8.0 25
5 298.0 29.8 183.8 3639.9 32.0 1188

1.3.3.3 Plot de K-Means 📈 [0.5 puntos]¶

Por último, Mr. Lepin, impaciente de no entender lo que usted intenta explicarle, le solicita que por favor muestre algún resultado "visual" de los grupos encontrados.

Para esto, grafique nuevamente las características encontradas usando T-SNE (no calcule de nuevo, simplemente utilice las proyecciones encontradas) y agregue las labels calculadas con kmeans como el argumento color.

Comente: ¿Se separan bien los distintos clusters en la visualización?

Respuesta:

In [15]:
X_reduced_with_cluster = X_reduced.copy()
X_reduced_with_cluster["Cluster"] = labels
X_reduced_with_cluster["Cluster"] = X_reduced_with_cluster.Cluster.astype("category")
fig = px.scatter(X_reduced_with_cluster, x="x", y="y", color="Cluster")
fig.update_layout(title="Espacio de dimensión reducida con T-SNE")

La separación es razonable en varias partes (secciones verdes aisladas), pero hay punto de muy baja separación. Esto puede ocurrir porque la reducción de dimensionalidad no capturó agrupaciones igual que KMeans.

Conclusión¶

Eso ha sido todo para el lab de hoy, recuerden que el laboratorio tiene un plazo de entrega de una semana y que los días de atraso no se pueden utilizar para entregas de lab solo para tareas. Cualquier duda del laboratorio, no duden en contactarnos por mail o U-cursos.

Gracias Totales!



Created in deepnote.com Created in Deepnote